package org.third.spring.boot.hello;

import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;

import javax.persistence.Entity;
import javax.persistence.EntityManagerFactory;
import javax.persistence.PersistenceContext;
import javax.sql.DataSource;

import org.hibernate.MultiTenancyStrategy;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.boot.spi.MetadataImplementor;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.cfg.Environment;
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
import org.hibernate.engine.jdbc.connections.spi.AbstractDataSourceBasedMultiTenantConnectionProviderImpl;
import org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider;
import org.hibernate.tool.hbm2ddl.SchemaUpdate;
import org.hibernate.tool.schema.TargetType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.orm.hibernate5.LocalSessionFactoryBean;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.third.spring.boot.hello.domain.PersistObject;

/**
 * Created by Huailong Tang(Jason) on 2017/10/16.
 */
@Configuration
@EnableConfigurationProperties({ JpaProperties.class })
@EnableTransactionManagement(proxyTargetClass = true)
@EnableJpaRepositories(basePackages = {
		"org.third.spring.boot.hello.domain" }, transactionManagerRef = "transactionManager")
public class InstallerJpaConfiguration {
	static final Logger logger = LoggerFactory.getLogger(InstallerJpaConfiguration.class);

	public class DataSourceBasedMultiTenantConnectionProviderImpl
			extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl {

		@Autowired
		private Map<String, DataSource> installerDataSource;

		@Override
		protected DataSource selectAnyDataSource() {
			initialize();
			return this.installerDataSource.get("DEFAULT");
		}

		public void initialize() {
			synchronized (this) {
				logger.info("postgres initialize ...");
				MetadataSources metadata = new MetadataSources(new StandardServiceRegistryBuilder()
						.applySetting(AvailableSettings.DIALECT, "org.hibernate.dialect.PostgreSQLDialect")
						.applySetting(AvailableSettings.DRIVER, dataSourceProperties.getDriverClassName())
						.applySetting(AvailableSettings.URL, dataSourceProperties.getUrl())
						.applySetting(AvailableSettings.USER, dataSourceProperties.getUsername())
						.applySetting(AvailableSettings.PASS, dataSourceProperties.getPassword()).build());

				ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(
						false);
				scanner.addIncludeFilter(new AnnotationTypeFilter(Entity.class));
				// use Deployment's package as basePackage
				for (BeanDefinition bd : scanner.findCandidateComponents(PersistObject.class.getPackage().getName())) {
					String name = bd.getBeanClassName();
					try {
						metadata.addAnnotatedClassName(name);
					} catch (Exception e) {
						logger.error("get database entity fail ...");
						e.printStackTrace();
					}
				}
				// Don't use SchemaExport (equal = create drop) ,ShemaUpdate used for
				// update
				MetadataImplementor args = (MetadataImplementor) metadata.buildMetadata();
				SchemaUpdate schemaUpdate = new SchemaUpdate();
				EnumSet<TargetType> targetTypes = EnumSet.copyOf(Arrays.asList(TargetType.values()));
				schemaUpdate.execute(targetTypes, args);
			}
		}

		@Override
		protected DataSource selectDataSource(String tenantId) {
			DataSource ds = this.installerDataSource.get(tenantId);
			// Logger.info("cannot found datasource with tenant={}", tenantId);
			if (ds == null && !this.installerDataSource.isEmpty()) {
				return this.installerDataSource.values().iterator().next();
			}
			return ds;
		}
	}

	public class CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver {

		String DEFAULT = "DEFAULT";

		@Override
		public String resolveCurrentTenantIdentifier() {
			return DEFAULT;
		}

		@Override
		public boolean validateExistingCurrentSessions() {
			return true;
		}
	}

	@Autowired
	private DataSourceProperties dataSourceProperties;

	@Autowired
	private JpaProperties jpaProperties;

	@Bean
	public MultiTenantConnectionProvider multiTenantConnectionProvider() {
		return new DataSourceBasedMultiTenantConnectionProviderImpl();
	}

	@Bean
	public CurrentTenantIdentifierResolver currentTenantIdentifierResolver() {
		return new CurrentTenantIdentifierResolverImpl();
	}

	@Bean(name = "installerDataSource")
	public Map<String, DataSource> installerDatasource() {
		Map<String, DataSource> datasources = new HashMap<>();
		return datasources;
	}

	@Primary
	@Bean
	@PersistenceContext
	public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(
			MultiTenantConnectionProvider multiTenantConnectionProvider,
			CurrentTenantIdentifierResolver currentTenantIdentifierResolver) {

		Map<String, Object> hibernateProps = new LinkedHashMap<>();
		hibernateProps.putAll(this.jpaProperties.getProperties());
		hibernateProps.put(Environment.MULTI_TENANT, MultiTenancyStrategy.DATABASE);
		hibernateProps.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProvider);
		hibernateProps.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, currentTenantIdentifierResolver);

		// new EntityManagerFactoryBuilderImpl(
		// new PersistenceUnitInfoDescriptor(info) {
		// @Override
		// public List<String> getManagedClassNames() {
		// return mergedClassesAndPackages;
		// }
		// }, properties).build();
		//
		LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
		em.setPackagesToScan(
				new String[] { org.third.spring.boot.hello.domain.PersistObject.class.getPackage().getName() });
		em.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
		em.setJpaPropertyMap(hibernateProps);

		return em;
	}

	@Bean
	public DataSource dataSource() {
		DriverManagerDataSource dataSource = new DriverManagerDataSource();
		dataSource.setDriverClassName(dataSourceProperties.determineDriverClassName());
		dataSource.setUrl(dataSourceProperties.determineUrl());
		dataSource.setUsername(dataSourceProperties.determineUsername());
		dataSource.setPassword(dataSourceProperties.determinePassword());
		// dataSource.setSchema(dataSourceProperties.getSchema().isEmpty()?null:dataSourceProperties.getSchema().get(0));
		return dataSource;
	}

	@Bean
	public LocalSessionFactoryBean sessionFactory(DataSource dataSource) {
		LocalSessionFactoryBean sessionFactoryBean = new LocalSessionFactoryBean();
		sessionFactoryBean.setDataSource(dataSource);
		// sessionFactoryBean.setMappingResources("classpath*:**/*.hbm.xml");
		Properties hibernateProperties = new Properties();
		hibernateProperties.putAll(jpaProperties.getProperties());

		sessionFactoryBean.setHibernateProperties(hibernateProperties);

		return sessionFactoryBean;
	}

	// @Bean
	// public HibernateTransactionManager transactionManager() {
	// HibernateTransactionManager transactionManager = new
	// HibernateTransactionManager();
	// transactionManager.setSessionFactory(sessionFactory().getObject());
	// return transactionManager;
	// }
	@Bean
	public EntityManagerFactory entityManagerFactory(LocalContainerEntityManagerFactoryBean entityManagerFactoryBean) {
		return entityManagerFactoryBean.getObject();
	}

	@Bean(name = "transactionManager")
	public PlatformTransactionManager txManager(EntityManagerFactory entityManagerFactory) {
		JpaTransactionManager tx = new JpaTransactionManager();
		tx.setEntityManagerFactory(entityManagerFactory);
		return tx;
	}

}
